﻿namespace Filters.Demo.CustomFilters
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Drawing.Imaging;
    using AForge.Imaging;
    using AForge.Imaging.Filters;

    /// <summary>
    /// SobelPointBasedFilter class
    /// 
    /// Each pixel of the result image is calculated as approximated absolute gradient
    /// magnitude for corresponding pixel of the source image:
    /// |G| = |Gx| + |Gy] ,
    /// where Gx and Gy are calculate utilizing Sobel convolution kernels:
    ///    Gx         Gy
    /// -1 0 +1    +1 +2 +1
    /// -2 0 +2     0  0  0
    /// -1 0 +1    -1 -2 -1
    ///
    /// Using the above kernel the approximated magnitude for pixel x,y is calculated using 
    /// the next equation:
    /// P1 P2 P3
    /// P8  x P4
    /// P7 P6 P5
    /// 
    /// |G| = |P1 + 2P2 + P3 - P7 - 2P6 - P5| +
    ///       |P3 + 2P4 + P5 - P1 - 2P8 - P7|
    /// </summary>
    public class SobelPointBasedFilter : IFilter, IFilterInformation
    {
        private Dictionary<PixelFormat, PixelFormat> formatTransalations = new Dictionary<PixelFormat, PixelFormat>();

        public SobelPointBasedFilter()
        {
            // initialize format translation dictionary
            this.formatTransalations[PixelFormat.Format8bppIndexed] = PixelFormat.Format8bppIndexed;
        }

        public SobelPointBasedFilter(IEdgePointsReceptor receptor)
            : this()
        {
            this.PointsReceptor = receptor;
        }

        public IEdgePointsReceptor PointsReceptor
        {
            get;
            set;
        }

        public Dictionary<PixelFormat, PixelFormat> FormatTransalations
        {
            get { return this.formatTransalations; }
        }

        public Bitmap Apply(Bitmap image)
        {
            if (!this.FormatTransalations.ContainsKey(image.PixelFormat))
            {
                throw new UnsupportedImageFormatException("The selected image processing routine may be applied to binary images only.\n\nUse grayscale and threshold filter before.");
            }

            // lock source bitmap data
            var srcData = image.LockBits(
                new Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly,
                image.PixelFormat);

            Bitmap dstImage = null;

            // get width and height
            var width = srcData.Width;
            var height = srcData.Height;

            // destination image format
            var dstPixelFormat = srcData.PixelFormat;

            // create new image of required format
            dstImage = (dstPixelFormat == PixelFormat.Format8bppIndexed) ?
                AForge.Imaging.Image.CreateGrayscaleImage(width, height) :
                new Bitmap(width, height, dstPixelFormat);

            // lock destination bitmap data
            var dstData = dstImage.LockBits(
                new Rectangle(0, 0, width, height),
                ImageLockMode.ReadWrite,
                dstPixelFormat);

            try
            {
                this.ProcessFilter(new UnmanagedImage(srcData), new UnmanagedImage(dstData), new Rectangle(0, 0, width, height));
            }
            finally
            {
                dstImage.UnlockBits(dstData);
                image.UnlockBits(srcData);
            }

            return dstImage;
        }

        public void Apply(UnmanagedImage sourceImage, UnmanagedImage destinationImage)
        {
            throw new NotImplementedException();
        }

        public UnmanagedImage Apply(UnmanagedImage image)
        {
            throw new NotImplementedException();
        }

        public Bitmap Apply(BitmapData imageData)
        {
            throw new NotImplementedException();
        }

        private unsafe void ProcessFilter(UnmanagedImage source, UnmanagedImage destination, Rectangle rect)
        {
            // processing start and stop X,Y positions
            var startX = rect.Left + 1;
            var startY = rect.Top + 1;
            var stopX = startX + rect.Width - 2;
            var stopY = startY + rect.Height - 2;

            var dstStride = destination.Stride;
            var srcStride = source.Stride;

            var dstOffset = dstStride - rect.Width + 2;
            var srcOffset = srcStride - rect.Width + 2;

            // data pointers
            var src = (byte*)source.ImageData.ToPointer();
            var dst = (byte*)destination.ImageData.ToPointer();

            // allign pointers
            src += (srcStride * startY) + startX;
            dst += (dstStride * startY) + startX;

            if (this.PointsReceptor != null)
            {
                this.PointsReceptor.MapedPoints.Clear();
            }

            // for each line
            for (var y = startY; y < stopY; y++)
            {
                // for each pixel
                for (var x = startX; x < stopX; x++, src++, dst++)
                {
                    // calculate gradient components
                    double gx = src[-srcStride + 1] + src[srcStride + 1] - src[-srcStride - 1] - src[srcStride - 1] + (2 * (src[1] - src[-1]));
                    double gy = src[-srcStride - 1] + src[-srcStride + 1] - src[srcStride - 1] - src[srcStride + 1] + (2 * (src[-srcStride] - src[srcStride]));

                    // calculate gradient magnitude
                    var g = Math.Min(255, Math.Abs(gy) + Math.Abs(gx));

                    // calculate gradient direction
                    double dir = gx > 0 ? Math.Atan(gy / gx) : 0;

                    // If magnitude differs from zero it means there is a change in gray level
                    if (g > 0)
                    {
                        var isNeighbor = false;

                        for (var i = x - 2; i <= x + 2; i++)
                        {
                            for (var j = y - 2; j <= y + 2; j++)
                            {
                                if (!((i == x) && (j == y)))
                                {
                                    // if one of the neighbors of the actual point (x,y) is in the list,
                                    // then the actual point is neighbor from an existing one
                                    if ((this.PointsReceptor != null) && this.PointsReceptor.MapedPoints.ContainsKey(new Point(i, j)))
                                    {
                                        isNeighbor = true;
                                    }
                                }
                            }
                        }

                        // actual point is only added if it isn't neighbor from an existing one
                        if (!isNeighbor)
                        {
                            if (this.PointsReceptor != null)
                            {
                                this.PointsReceptor.MapedPoints.Add(
                                    new Point(x, y),
                                    new EdgePoint(new Point(x, y), g, dir));
                            }

                            *dst = (byte)g;
                        }
                        else
                        {
                            // paint in black points which are not added
                            g = 0;
                            *dst = (byte)g;
                        }
                    }
                    else
                    {
                        // paint in black points not belonging to an edge
                        g = 0;
                        *dst = (byte)g;
                    }
                }

                src += srcOffset;
                dst += dstOffset;
            }
        }
    }
}
